构造函数是特殊的成员函数
构造函数是特殊的成员函数,与其他成员函数不同,构造函数和类同名,而且没有返回类型。而与其他成员函数相同的是,构造函数也有形参表(可能为空)和函数体。一个类可以有多个构造函数,每个构造函数必须有与其他构造函数不同数目或类型的形参。
构造函数的形参指定了创建类类型对象时使用的初始化式。通常,这些初始化式会用于初始化新创建对象的数据成员。构造函数通常应确保其每个数据成员都完成了初始化。
构造函数是特殊的成员函数,只要创建类类型的新对象,都要执行构造函数。构造函数的工作是保证每个对象的数据成员具有合适的初始值。
用于初始化一个对象的实参类型决定使用哪个构造函数。
无参构造函数:定义对象的时候,对象后不能加括号。如:class stu{ stu(){"无参构造函数"}}; stu st;(正确定义对象)。
深拷贝意味着拷贝了资源(new了新内存空间)和指针,而浅拷贝只是拷贝了指针,没有拷贝资源。这样会使得两个指针指向同一份资源,造成对同一份析构两次,程序崩溃。临时对象的开销比局部对象小些。
调用默认的拷贝构造函数相当于执行下列操作:
arr1.low = arr2.low;
arr1.high = arr2.high;
arr1.storage = arr2.storage;
前两个操作没有问题,第三个操作中,storage是一个指针,第三个操作意味着使arr1的storage指针和arr2的storage指针指向同一块空间。
一个对象的修改将会影响另一个对象
如果两个对象的作用域不同,当一个对象析构时,另一个对象也将丧失它的空间
构造函数和析构函数的作用是什么?它们各有什么特征?
构造函数是在对象定义时自动执行,为对象赋初值。析构函数是对象销毁时自动调用,
做一些善后工作。构造函数的名字就是类名,析构函数的名字是波浪号加类名。构造函数和析构函数都不需要写函数的返回类型。对象可能有不同的构造方法,所以类可以有一组重载的构造函数,但析构函数只能有一个。构造函数还可以有一个初始化列表。
什么情况下类必须定义自己的复制构造函数?
如果类的数据成员中含有指针,而指针指向的是一个动态变量,必须自己定义复制构造函数。或对复制构造有其他特殊的要求也需要定义复制构造函数。
复制构造函数的参数为什么一定要用引用传递,而不能用值传递?
值传递的参数在参数传递时有一个构造过程,即用实际参数的值构造形式参数,这个构造过程是由复制构造函数完成的。如果将复制构造函数的参数设计成值传递,会引起复制构造函数的递归调用。
构造函数为什么要有初始化列表?
构造函数的初始化列表可以将数据成员的构造和赋初值一起完成,提高对象构造的时间性能。除此之外,还有两种情况必须用初始化列表。第一种情况是数据成员中含有一些不能用赋值操作进行赋值的数据成员,例如常量数据成员或对象数据成员,这时必须在初始化列表中调用数据成员所属类型的构造函数来构造它们。第二种情况是在用派生的方法定义一个类时,派生类对象中的基类部分必须在构造函数的初始化列表中调用基类的构造函数完成。
构造函数和析构函数是特殊的成员函数
构造函数:为对象分配空间,进行初始化。
析构函数:执行与构造函数相反的操作,通常执行一些清理工作,如释放分配给对象的空间等。
构造函数还有一个与普通函数不同的地方,就是可以包含一个构造函数初始化列表。
构造函数初始化列表位于函数头和函数体之间。它以一个冒号开头,接着是一个以逗号分隔的数据成员列表
每个数据成员的后面跟着一个放在圆括号中的对应于该数据成员的构造函数的实际参数表。
如IntArray的构造函数可写为
IntArray :: IntArray(int lh, int rh): low(lh), high(rh)
{ storage = new int [high - low + 1]; }
事实上,不管构造函数中有没有构造函数初始化列表,在执行构造函数体之前,都要先初始化每个数据成员。
在构造函数初始化列表中没有提到的数据成员,系统会用该数据成员对应类型的默认构造函数对其初始化。
显然利用初始化列表可以提高构造函数的效率。在初始化的时候,同时完成了赋初始的工作。
必须用初始化的情况:
数据成员不是普通的内置类型,而是某一个类的对象,可能无法直接用赋值语句在构造函数体中为它赋初值
类包含了一个常量的数据成员,常量只能在定义时对它初始化,而不能对它赋值。因此也必须放在初始化列表中。
拷贝构造函数以一个同类对象引用作为参数,它的原型为:
类名(const <类名> &);
如果用户没有定义拷贝构造函数,系统会定义一个缺省的拷贝构造函数。该函数将已存在的对象原式原样地复制给新成员
何时需要自定义拷贝构造函数
一般情况下,默认的拷贝构造函数足以满足要求。
但某些情况下可能需要设计自己的拷贝构造函数。
例如,我们希望对IntArray类增加一个功能,能够定义一个和另一个数组完全一样的数组。但默认的拷贝构造函数却不能胜任。如果正在构造的对象为arr1,作为参数的对象是arr2,调用默认的拷贝构造函数相当于执行下列操作:
arr1.low = arr2.low;
arr1.high = arr2.high;
arr1.storage = arr2.storage;
前两个操作没有问题,第三个操作中,storage是一个指针,第三个操作意味着使arr1的storage指针和arr2的storage指针指向同一块空间(特别是当有动态内存申请时)。
使用同一块空间的问题:
一个对象的修改将会影响另一个对象
如果两个对象的作用域不同,当一个对象析构时,另一个对象也将丧失它的空间
IntArray(const IntArray &arr) { low = arr.low; high = arr.high; storage = new int [high – low + 1]; for (int i = 0; i < high –low + 1; ++i) storage[i] = arr.storage[i]; }
拷贝构造函数的应用场合
对象定义时
把对象作为参数传给函数时
把对象作为返回值时
拷贝构造函数用于对象构造时有两种用法:直接初始化和拷贝初始化。
直接初始化将初始值放在圆括号中,直接调用与实参类型相匹配的构造函数。如
IntArray array2(array1);
拷贝初始化是用“=”符号。当使用拷贝初始化时,首先会用“=”右边的表达式构造一个临时对象,再调用拷贝构造函数将临时对象复制到正在构造的对象。如
IntArray array = IntArray(20,30);
例: class A { A(int size); //构造函数 const int SIZE; } A::A(int size) : SIZE(size) //构造函数的初始化表 {…} A a(100); //对象a的SIZE的值为100 A b(200); //对象b的SIZE的值为200
如果构造函数中存在动态内存申请时,请要考虑深拷贝和浅拷贝了。
拷贝构造函数与赋值运算符的区别。
CTest a;
CTest b(a); //拷贝构造,因为B不存在,所以称为构造
CTtest c = a; //调用拷贝构造函数,也是因为C不存在;
c = b; //调用赋值运算符,此时对象C已经存在
析构函数(destructor)是类的成员函数,在类的对象离开作用域之后自动调用。例如,假定类类型的对象是某函数的局部变量,调用析构函数就是函数调用结束前采取的最后一项行动。析构函数销毁由对象创建的任何动态变量,将其占用的内存还给自由存储。析构函数还可执行其他清理工作。析构函数的名称必须由~符号和类名构成。
拷贝构造函数获取一个传引用参数,该参数具有与类相同的类型。参数必须传引用。通常,该参数也是常量参数,要在它前面附加参数修饰符const。只要某个函数返回类类型的值,就会自动调用那个类的拷贝构造函数。在类类型的传值参数位置“插入”实参时,也会自动调用拷贝构造函数。拷贝构造函数还可采取与其他构造函数相同的方式来使用。
凡是使用了指针和操作符new 的类,都应该有一个拷贝构造函数。
析构函数是类的一种特殊成员函数,在类的对象离开作用域时自动调用。析构函数的主要使命是将内存还给自由存储,使内存得以重用。
拷贝构造函数是一种特殊构造函数,它有一个参数,该参数具有与类相同的类型。定义了拷贝构造函数之后,只要函数返回类类型的值,或者在类类型的一个传值参数位置“插入”一个实参,就会自动调用这个构造函数。使用了指针和操作符new 的任何类都应包含一个拷贝构造函数。
C++支持使用赋值语法或者构造函数语法调用拷贝构造函数。
拷贝构造函数的形参必须采用别名(引用)生成方式,如果是赋值方式,则会造成拷贝构造函数的无穷递归调用。
默认拷贝构造函数:用源对象的每个数据成员来初始化目标对象的对应数据成员。
如果数据成员是一个指向动态数组的指针,那么,默认拷贝会导致新对象的指针和源对象的指针指向同一个动态数组。当新对象或者源对象其中之一消亡时,会通过delete指针释放掉动态数组,这时,另一个对象的指针就指向了无效的动态数组,成为虚悬指针,此时很有必要提供自己的拷贝构造函数。
拷贝构造函数不仅是在我们定义对象时会被调用,实际上,在需要通过同类对象克隆产生新对象的任何时候,都可能会调用拷贝构造函数。常见两种情况:
1 函数按值返回一个对象;
2 调用调用时克隆生成形参对象;
所谓构造式(constructor),就是对象诞生后第一个执行(并且是自动执行)的函数,它的函数名称必定要与类别名称相同。相对于构造式,自然就有个析构式(destructor),也就是在对象行将毁灭但未毁灭之前一刻,最后执行(并且是自动执行)的函数,它的函数名称必定要与类别名称相同,再在最前面加一个~ 符号。
一个有着阶层架构的类别群组,当衍生类别的对象诞生之时,构造式的执行是由最基础类别(most based)至最尾端衍生类别(most derived);当对象要毁灭之前,析构式的执行则是反其道而行。
对于全域对象,程序一开始,其构造式就先被执行(比程序进入点更早);程序即将结束前其析构式被执行。
对于区域对象,当对象诞生时,其构造式被执行;当程序流程将离开该对象的存活范围(以至于对象将毁灭),其析构式被执行。
对于以new 方式产生出来的区域对象,当对象诞生时其构造式被执行。析构式则在对象被delete 时执行。
指针在堆上new出一块内存时,需要手动释放这块内存,当异常发生时,即使有释放的代码,但也有可能因为异常而跳过释放内存的代码,而产生内存泄漏。
因为你在类中是不能直接给private中的类成员赋值的(类声明是不分配内存空间的,要在实例化时才分配内存空间。),所以就靠构造函数,不过要注意有参构造和无参构造。
对于在全局作用域中定义的对象,它们的构造函数是在文件中所有其他函数(包括main)开始执行之前被调用的(但无法保证不同文件的全局对象构造函数的执行顺序)。对应的析构函数是在终止main之后调用的。
exit函数会迫使程序立即终止,而不会执行自动对象的析构函数。这个函数经常用来在检测到输入错误或者程序所处理的文件无法打开时终止程序。
abort函数与exit函数功能相似,但它会迫使程序立即终止,而不允许调用任何对象的析构函数。abort函数通常用来表明程序的非正常终止。
自动局部变量的构造函数是在程序的执行到达定义这个对象的位置时调用的,而对应的析构函数是在程序离开这个对象的作用域时调用的(即定义这个对象的代码完成了执行)。每次执行进入和离开自动对象的作用域时,都会调用它的构造函数和析构函数。如果程序调用了exit或abort函数而终止,则不会调用自动对象的析构函数。
静态局部对象的析构函数只调用一次,即执行首次到达定义这个对象的位置时。对应的析构函数是在main终止或程序调用exit函数时调用的。
拷贝构造函数生成新的类对象,而赋值运算符不能。当进行一个类的实例初始化时,调用的是构造函数,但如是用其他实例来初始化,则调用拷贝构造函数,非初始化时对这个实例进行赋值调用的是赋值运算符。
当类中的数据成员中使用new运算符,动态地申请存储空间进行赋初值时,必须在类中显式地定义一个完成拷贝功能的构造函数,以便正确实现数据成员的复制。
派生类构造函数要负责调用基类的构造函数
C++本身就规定创建子类对象的时,先调用基类的构造函数,然后再调用自己类的构造函数。当我们的基类没有自己定义构造函数时候(就是系统默认的构造函数)时。创建子类对象会先默认调用基类的默认构造函数 。但是,当我们的基类自己定义了构造函数,(可能定义了很多个)此时不会再自动生产默认构造。但是它不知道应该调用基类中的哪个构造,所以需要手动指定。
在释放派生类对象时,析构函数函数的执行顺序是:先执行派生类的析构函数,然后执行成员对象的析构函数,最后执行基类的析构函数。
C++本身就规定创建子类对象的时,先调用基类的构造函数,然后再调用自己类的构造函数。当我们的基类没有自己定义构造函数时候(就是系统默认的构造函数)时。创建子类对象会先默认调用基类的默认构造函数
当没有显式调用指定形式的构造函数。系统自动调用无参构造函数,如果没有为类指定此构造函数,则系统自动为其生成一个最简单的无参构造函数。
C++中类的成员对象比类的对象先初始化。
当没有显式调用指定形式的构造函数。系统自动调用无参构造函数,如果没有为类指定此构造函数,则系统自动为其生成一个最简单的无参构造函数。
派生类初始化三部分内容:
1 新增内嵌对象以外的数据成员
2 新增的内嵌对象;
3 基类数据成员(不包括基类内嵌对象);
第二和第三在初始化列表中调用构造完成;
在参数表中包含上述三类参数的实参;
默认拷贝构造函数按成员逐项进行复制可能导致对象的浅拷贝,这时副本对象的指针数据成员与原始对象的指针数据成员指向相同的内存块。深拷贝是指对象的副本与原始对象没有共享的内存块。默认赋值运算符成员函数只提供按成员逐项进行的复制。
如果类中有虚函数,那么也应当将析构函数声明为虚函数。然而,有些程序员认为,为了安全起见,总是应当将析构函数声明为虚函数。因为当有多继承时,基类的析构函数定义为virtual时,编译器才会产生自动调用子类析构函数的行为。
派生类中有资源需要回收,而在编程中采用多态,由基类的指针指向派生类,则在释放的时候,如果基类的析构函数不是virtual,则派生类的析构函数得不到释放
C++中基类采用virtual虚析构函数是为了防止内存泄漏。具体地说,如果派生类中申请了内存空间,并在其析构函数中对这些内存空间进行释放。假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会触发动态绑定,因而只会调用基类的析构函数,而不会调用派生类的析构函数。那么在这种情况下,派生类中申请的空间就得不到释放从而产生内存泄漏。所以,为了防止这种情况的发生,C++中基类的析构函数应采用virtual虚析构函数。
Examples of actions you might want to take in a destructor include releasing file handles, flushing network sockets, and freeing dynamic objects.
派生类的构造函数:不继承,其参数列表要考虑对基类及其本类的数据成员进行初始化,对基类数据成员的初始化在初始化列表中调用基类的成员函数。数据成员是对象时也要在初始化列表中调用初始化列表进行初始化,另外,const成员和reference成员也要在初始化列表中初始化,其它部分可以在构造函数的函数体中初始化。
浅拷贝
实现对象间数据元素的一一对应复制。
深拷贝
当被复制的对象数据成员是指针类型时,不是复制该指针成员本身,而是将指针所指的对象进行复制。
何时需要虚析构函数?
当你可能通过基类指针删除派生类对象时
如果你打算允许其他人通过基类指针调用对象的析构函数(通过delete这样做是正常的),并且被析构的对象是有重要的析构函数的派生类的对象,就需要让基类的析构函数成为虚拟的。
先构造父类,再构造成员变量、最后构造自己
先析构自己,在析构成员变量、最后析构父类
构造函数应完成简单有效的功能,不应完成复杂的运算和大量的内存管理。
如果该类有相当多的初始化工作,应生成专门的Init(…)函数,不能完全在构造函数中进行,因为构造函数没有返回值,不能确定初始化是否成功。
构造函数应完成简单有效的功能,不应完成复杂的运算和大量的内存管理。
如果该类有相当多的初始化工作,应生成专门的Init(…)函数,不能完全在构造函数中进行,因为构造函数没有返回值,不能确定初始化是否成功。
析构函数一般做的事情:
1 释放堆内存;
2 关掉一个文件;
3 关掉一个窗口;
4 释放一个lock;
5 对象计数器自减,如shared_ptr。
The primary role of the destructor is to free any heap memory allocated by the object.
基类的析构函数不是虚函数,会带来什么问题?
在实现多态时,如果是由基类指针指向在堆上动态创建派生对象时,如果delete此虚类指针,则基类的析构函数被调用,并不会调用派生类的析构函数,如果派生类中有在堆上动态创建的数据时,则会出现内存泄露。C++编译器的做法时,如果基类的析构函数前面有用virtual修饰,编译器会自动去调用派生类的析构函数(实现动态绑定,如果其它成员函数的动态绑定一样),做内存释放的工作。